本页目录

unsafe 块与 Soundness 契约

unsafe 关键字不"禁用"安全检查,而是把本由编译器保证的安全契约转交给程序员保证——你可以解引用裸指针、调用 unsafe 函数、访问可变静态变量,但你必须确保无论调用方怎么做,都不会触发 UB(未定义行为)。写 unsafe 代码的真正难点不是"能写出来",而是为它封装一个安全的抽象外壳。

unsafe 不是"禁用所有安全检查"

unsafe {} 块只开启 5 项能力,其余的 Rust 安全检查依然生效:

  1. 解引用裸指针 (*const T, *mut T)
  2. 调用 unsafe fn
  3. 访问或修改 static mut 变量
  4. 实现 unsafe trait (如 Send, Sync, GlobalAlloc)
  5. 访问 union 的字段

Borrow checker 在 unsafe 块内仍然有效⁠——你不能创建两个 &mut T 指向同一数据,这是 UB,编译器在 unsafe 中依然会拒绝。

理解 unsafe 的关键:它不是"我来承担所有安全责任",而是"我承担编译器无法自动验证的那部分责任"。

Soundness: 安全的边界

unsafe fn 的签名定义了契约⁠。调用者必须满足前置条件,否则可能产生 UB。标准库中到处是这样的模式:

// 对外安全, 内部 unsafe — 调用者只需要传对参数, 不需要写 unsafe
pub fn split_at_mut(&mut self, mid: usize) -> (&mut [T], &mut [T]) {
    assert!(mid <= self.len());      // 运行时检查 — 这是安全边界!
    let ptr = self.as_mut_ptr();
    unsafe { (from_raw_parts_mut(ptr, mid),
              from_raw_parts_mut(ptr.add(mid), self.len() - mid)) }
}

如果 assert 通过,下面的 unsafe 操作就是 sound 的——mid <= len 保证了两段 slice 不重叠。如果 assert 失败,直接 panic——不会进入 unsafe 区域。这是标准库最常用的模式:safe code 验证条件,unsafe code 在条件成立的前提下执行⁠。

关键:unsafe 代码的 soundness 不仅依赖于 unsafe 块内的逻辑,还依赖于外部 safe code 维护的不变量⁠。如果你写的 unsafe 代码假设了某个数据结构的不变量(如 len <= cap),那么外部所有修改该数据结构的 safe 代码都必须维护这个不变量。

UB (Undefined Behavior): 必须避免的清单

  • 解引用空指针、悬垂指针、未对齐指针
  • 违反借用规则(即使在 unsafe 中)
  • 数据竞争
  • 用错误的 ABI 调用函数
  • 产生不合法的值:bool 非 0 非 1、引用为空、char 是 surrogate
  • unwind 穿过 FFI 边界(extern "C" 函数中 panic)

Miri: 自动化 UB 检测

Miri 在 MIR 级别解释执行 Rust,追踪 provenance 和 borrow 状态:

cargo +nightly miri test

可以检测:use-after-free、out-of-bounds access、data races、alignment violations。任何有 unsafe 的 crate 都应该跑 Miri——它不能证明 soundness,但能发现明显的 UB。

参考

  • Rustonomicon: "What Unsafe Can Do", Soundness requirements
  • Rust Reference: Behavior considered undefined
  • Miri: github.com/rust-lang/miri

Keywords: unsafe, soundness, UB, Miri, safety contract, invariant, raw pointer